# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: inkyu

import datetime
import logging
import json

import requests
from typing import List, Optional, Union

from tornado import gen

from coin.base.param_util import to_list
from coin.base.timestamp import get_timestamp
import coin.flow.topic_map as topic_map

from coin.exchange.upbit_v1.rest.private_client import UpbitPrivateClient
from coin.exchange.upbit_v1.rest.private_parser import UpbitExchangeOrderState


class _UpbitPrivateQueryContext(object):
  def __init__(self, config, ioloop, publisher, logger, symbol, period, *, max_connections=6):
    self._config = config
    self._ioloop = ioloop
    self._publisher = publisher
    self._logger = logger
    self._symbol = symbol
    self._period = period

    self._stopped = False
    self._querying = False
    self._upbit_client = UpbitPrivateClient(config, max_connections=4)

  def stop(self):
    self._stopped = True

  def start(self):
    if self._stopped:
      return
    self._ioloop.add_callback(self._run_query)
    self._ioloop.add_timeout(self._period, self._on_timeout)

  def _on_timeout(self):
    if self._stopped:
      return
    if not self._querying:
      self._ioloop.add_callback(self._run_query)
    else:
      self._logger.warning('Still qeurying. Skip querying this time')
    self._ioloop.add_timeout(self._period, self._on_timeout)

  def _on_response(self, query, timestamp, response, *, state=None):
    if response.status_code != 200:
      self._logger.warning('Response: %d %s', response.status_code, response.content)
      return

    data = json.loads(response.content)
    message = {'query': query, 'data': data}
    if state:
      message['state'] = state
    self._publisher.write(timestamp, message)

  @gen.coroutine
  def _query_open_orders(self, *, state):
    response = None
    for try_n in range(0, 3):  # Try 3 times on error
      if self._stopped:
        return

      try:
        response = yield self._upbit_client.orders(state=state.to_str())
      except requests.exceptions.RequestException as e:
        self._logger.exception('RequestException')
        continue
      if response.status_code != 200:
        self._logger.error('Query error: %s', response.content)
        continue

      timestamp = get_timestamp()
      break

    if self._stopped:
      return

    try:
      if response is not None:
        self._on_response('orders', timestamp, response, state=state)
      else:
        self._logger.error('Retry failure')
    except Exception:
      self._logger.exception('Exception on procession a response: %s' % response)

  @gen.coroutine
  def _query_accounts(self):
    response = None
    for try_n in range(0, 3):  # Try 3 times on error
      if self._stopped:
        return

      try:
        response = yield self._upbit_client.accounts()
      except requests.exceptions.RequestException as e:
        self._logger.exception('RequestException')
        continue
      if response.status_code != 200:
        self._logger.error('Query error: %s', response.content)
        continue

      timestamp = get_timestamp()
      break

    if self._stopped:
      return

    try:
      if response is not None:
        self._on_response('accounts', timestamp, response)
      else:
        self._logger.error('Retry failure')
    except Exception:
      self._logger.exception('Exception on procession a response: %s' % response)

  @gen.coroutine
  def _run_query(self):
    self._querying = True
    self._logger.info('Querying %d symbols' % len(self._symbol))
    try:
      yield ([self._query_accounts()]
             + [self._query_open_orders(state=state) for state in UpbitExchangeOrderState])
    finally:
      self._querying = False
      if self._stopped:
        self._logger.info('Stopped')


class UpbitPrivateQueryer(object):
  def __init__(self,
               config,
               queue,
               worker_id: str,
               symbol: Union[List[str], str],
               period=None,
               logger=None):
    self._config = config
    self._logger = logger or logging.getLogger(__name__)
    self._symbol = to_list(symbol)
    self._period = period or datetime.timedelta(seconds=1)

    self._context = None
    self._publisher = None
    self._init_publisher(queue, worker_id)

  def __del__(self):
    self.stop()
    self._publisher.close()

  def _init_publisher(self, queue, worker_id):
    topic_id = topic_map.search_topic_id('upbit_rest_private_%s' % worker_id)[0][1]
    self._publisher = queue.get_writer(topic_id=topic_id)

  def start(self, ioloop, later_td: Optional[datetime.timedelta] = None):
    assert self._context is None
    self._context = _UpbitPrivateQueryContext(self._config,
                                              ioloop,
                                              self._publisher,
                                              self._logger,
                                              self._symbol,
                                              self._period)

    if later_td is None:
      self._context.start()
    else:
      ioloop.add_timeout(later_td, self._context.start)

  def stop(self):
    if self._context is not None:
      self._context.stop()
      self._context = None
